﻿using System;
using System.Diagnostics.Contracts;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using Ping.Core;
using Ping.Server.Jobs;
using log4net;

namespace Ping.Server
{
	/// <summary>
	/// Represents the connection between a client and this server and handles the entire communication.
	/// </summary>
	public sealed class Connection
	{
		public const int MaxExceptions = 10;

		private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

		private readonly EndPoint _remote;
		private readonly IChannel _channel;
		private readonly JobScheduler _scheduler;

		private DateTime _lastOperation;
		private int _numExceptions;
		private int _currentId;

		public event Action<Connection> Closed;

		/// <summary>
		/// Initializes this connection.
		/// Start must be called in order to start the connection.
		/// </summary>
		/// <param name="client"></param>
		/// <param name="scheduler"></param>
		public Connection(TcpClient client, JobScheduler scheduler)
		{
			if (client == null)
				throw new ArgumentNullException();

			_lastOperation = DateTime.Now;
			_scheduler = scheduler;
			_remote = client.Client.RemoteEndPoint;
			_channel = new Channel(client.Client);
		}

		public DateTime LastOperation
		{
			get { return _lastOperation; }
		}

		public EndPoint RemoteEndPoint
		{
			get { return _remote; }
		}

		/// <summary>
		/// Performs the entire request ==> response game.
		/// The method blocks until the response has been sent.
		/// </summary>
		public void Start()
		{
			_channel.BeginReceive(null, RequestReceived);
		}

		private void RequestReceived(IAsyncResult result)
		{
			_lastOperation = DateTime.Now;

			try
			{
				Data d = _channel.EndReceive(result);
				if (!(d is Request))
					throw new ReadRequestException();

				Data data = PerformRequest((Request)d);
				_channel.BeginSend(data, null, EndSend);

				RateConnection();

				// Let's wait for the next request...
				_channel.BeginReceive(null, RequestReceived);
			}
			catch(Core.SocketException e)
			{
				Log.DebugFormat("Caught exception: {0}", e);
				Log.WarnFormat("Aborting connection to client ({0}) because on a socket error", _remote);
			}
			catch (IOException e)
			{
				// An IOException is expected to happe sometimes (for example because of a forcefull disconnect)
				// and therefore are not reported using the critical logger
				Log.DebugFormat("Caught exception: {0}", e);
				Log.InfoFormat("Aborting connection to client ({0}) because of an I/O error", _remote);

				Close();
			}
			catch(ConnectionClosedException e)
			{
				Log.InfoFormat("Aborting connection to client ({0}): The client probably dropped it", _remote);
			}
			catch (ConnectionAbortException e)
			{
				Log.WarnFormat("Aborting connection to client ({0}) because the client doesn't behave as expected", _remote);

				Close();
			}
			catch (Exception e)
			{
				// Any unexpected exception causes the server to close the connection
				Log.ErrorFormat("Aborting connection to client ({0}) because of an unexpected error: {1}", _remote, e);
				Close();
			}
		}

		private void EndSend(IAsyncResult result)
		{
			_lastOperation = DateTime.Now;

			try
			{
				_channel.EndSend(result);
			}
			catch (IOException e)
			{
				// An IOException is expected to happe sometimes (for example because of a forcefull disconnect)
				// and therefore are not reported using the critical logger
				Log.DebugFormat("Caught exception on connection to {0}: {1}", _remote, e);
				Log.InfoFormat("Aborting connection to client ({0}) because of an I/O error", _remote);

				Close();
			}
			catch (ConnectionAbortException e)
			{
				Log.WarnFormat("Aborting connection to client ({0}) because the client doesn't behave as expected", _remote);

				Close();
			}
			catch (Exception e)
			{
				// Any unexpected exception causes the server to close the connection
				Log.ErrorFormat("Aborting connection to client ({0}) because of an unexpected error: {1}", _remote, e);
				Close();
			}
		}

		/// <summary>
		/// Closes the connection.
		/// </summary>
		public void Close()
		{
			_channel.Disconnect();
		}

		/// <summary>
		/// Performs the requested job and returns the result as serialized xml.
		/// </summary>
		/// <param name="request"></param>
		/// <returns></returns>
		private Result PerformRequest(Request request)
		{
			Job job = _scheduler.GetJob(request);

			Log.InfoFormat("Performing {0} Job for client ({1})", job.Name, _remote);

			var result = job.Perform(request);
			result.Id = _currentId++;

			return result;
		}

		private void HandleExceptionDuringRequest(Exception e)
		{
			++_numExceptions;

			OperationResult operation;
			if (e is ServerOperationException)
				operation = new OperationResult { Status = ServerStatus.OperationError };
			else if (e is ReadRequestException || e is ArgumentException)
				operation = new OperationResult { Status = ServerStatus.InvalidRequest };
			else
				operation = new OperationResult {Status = ServerStatus.InternalServerError};

			if (Log.IsDebugEnabled)
				Log.DebugFormat("Caught exception on connection to {0} while waiting for / reading request: {1}", _remote, e);
			if (Log.IsInfoEnabled)
				Log.InfoFormat("Invalid request on connection to client ({0})", _remote);

			_channel.BeginSend(operation, null, EndSend);
		}

		private void RateConnection()
		{
			if (_numExceptions > MaxExceptions)
				throw new ConnectionAbortException("The client triggered too many exceptions");
		}
	}
}